/*
 * Copyright (C) 2008-2010 Martin Riesz <riesz.martin at gmail.com>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.pneditor.petrinet;

import java.awt.Graphics;
import java.awt.Point;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.pneditor.util.GraphicsTools;

/**
 *
 * @author Martin Riesz <riesz.martin at gmail.com>
 */
public abstract class Edge extends Element {
	
	private Node source;
	private Node destination;
	private List<Point> breakPoints = new LinkedList<Point>();
	
	public List<Point> getBreakPoints() {
		return Collections.unmodifiableList(breakPoints);
	}

	public void removeBreakPoints() {
		breakPoints.clear();
	}
	
	public List<Point> getBreakPointsCopy() {
		List<Point> newBreakPoints = new LinkedList<Point>();
		for (Point breakPoint : breakPoints) {
			newBreakPoints.add(breakPoint.getLocation()); //getLocation because Point is mutable
		}
		return newBreakPoints;
	}
	
	public void setBreakPoints(List<Point> breakPoints) {
		this.breakPoints.clear();
		for (Point breakPoint : breakPoints) {
			this.breakPoints.add(breakPoint.getLocation()); //getLocation because Point is mutable
		}
	}
	
	protected void reverseBreakPoints() {
		Collections.reverse(breakPoints);
	}
	
	public Node getSource() {
		return source;
	}
	
	public void setSource(Node source) {
		this.source = source;
	}
	
	public Node getDestination() {
		return destination;
	}
	
	public void setDestination(Node destination) {
		this.destination = destination;
	}
	
	protected Point getLabelPoint(Point arrowTip) {
		final Point labelPoint = new Point();
		if (breakPoints.isEmpty()) {
			labelPoint.x = getStart().x + (arrowTip.x - getStart().x) * 2 / 3;
			labelPoint.y = getStart().y + (arrowTip.y - getStart().y) * 2 / 3 - 3;
		}
		else {
			final Point lastBreakPoint = breakPoints.get(breakPoints.size() - 1);
			labelPoint.x = lastBreakPoint.x + (arrowTip.x - lastBreakPoint.x) * 1 / 2;
			labelPoint.y = lastBreakPoint.y + (arrowTip.y - lastBreakPoint.y) * 1 / 2 - 3;
		}
		return labelPoint;
	}
	
	protected Point computeArrowTipPoint() {
		Point arrowTip = new Point(getEnd());
		if (getDestination() == null) {
			return arrowTip;
		}
		else { //Thanks to http://www.cs.unc.edu/~mcmillan/comp136/Lecture6/Lines.html
			int x0 = getLastBreakPoint().x;
			int y0 = getLastBreakPoint().y;
			int x1 = getEnd().x;
			int y1 = getEnd().y;

			int dy = y1 - y0;
			int dx = x1 - x0;
			int stepx, stepy;

			if (dy < 0) { dy = -dy;  stepy = -1; } else { stepy = 1; }
			if (dx < 0) { dx = -dx;  stepx = -1; } else { stepx = 1; }
			dy <<= 1;
			dx <<= 1;

			if (dx > dy) {
				int fraction = dy - (dx >> 1);
				while (x0 != x1) {
					if (fraction >= 0) {
						y0 += stepy;
						fraction -= dx;
					}
					x0 += stepx;
					fraction += dy;
					if (getDestination().containsPoint(x0, y0)) {
						return arrowTip;
					}
					arrowTip = new Point(x0, y0);
				}
			} else {
				int fraction = dx - (dy >> 1);
				while (y0 != y1) {
					if (fraction >= 0) {
						x0 += stepx;
						fraction -= dy;
					}
					y0 += stepy;
					fraction += dx;
					if (getDestination().containsPoint(x0, y0)) {
						return arrowTip;
					}
					arrowTip = new Point(x0, y0);
				}
			}
		}
		return arrowTip;
	}
	
	@Override
	public Point getStart() {
		return getSource() != null ? getSource().getCenter() : super.getStart();
	}
	
	@Override
	public Point getEnd() {
		return getDestination() != null ? getDestination().getCenter() : super.getEnd();
	}
	
	@Override
	public void moveBy(int dx, int dy) {
		super.moveBy(dx, dy);
		for (Point breakPoint : breakPoints) {
			breakPoint.translate(dx, dy);
		}
	}
	
	public static final int nearTolerance = 10;
	
	@Override
	public boolean containsPoint(int x, int y) {
		final Point testPos = new Point(x, y);
		Point previous = getStart();
		for (Point breakPoint : breakPoints) {
			if (GraphicsTools.isPointNearSegment(previous, breakPoint, testPos, nearTolerance)) {
				return true;
			}
			previous = breakPoint;
		}
		return GraphicsTools.isPointNearSegment(previous, getEnd(), testPos, nearTolerance);
	}
	
	public Point addOrGetBreakPoint(Point newPoint) {
		for (Point breakPoint : breakPoints) {
			if (GraphicsTools.isPointNearPoint(newPoint, breakPoint, nearTolerance)) {
				return breakPoint;
			}
		}

		if (breakPoints.isEmpty()) {
			breakPoints.add(newPoint);
		}
		else {
			Point previous = getStart();
			for (int i = 0; i < breakPoints.size(); i++) {
				if (GraphicsTools.isPointNearSegment(previous, breakPoints.get(i), newPoint, nearTolerance)) {
					breakPoints.add(i, newPoint);
					return newPoint;
				}
				previous = breakPoints.get(i);
			}
			if (GraphicsTools.isPointNearSegment(previous, getEnd(), newPoint, nearTolerance)) {
				breakPoints.add(newPoint);
			}
		}
		return newPoint;
	}

	public void addDistantBreakPointToEnd(Point newPoint) {
		breakPoints.add(newPoint);
	}

	public void addDistantBreakPointToBeginning(Point newPoint) {
		breakPoints.add(0, newPoint);
	}
	
	public void cleanupUnecessaryBreakPoints() {
		Point previous = getStart();
		for (int i = 0; i < breakPoints.size(); i++) {
			Point current = breakPoints.get(i);
			Point next = i < (breakPoints.size() - 1) ? breakPoints.get(i + 1) : getEnd();
			final int tolerance = Math.round(0.1f * (float)previous.distance(next));
			if (GraphicsTools.isPointNearSegment(previous, next, current, tolerance)) {
				breakPoints.remove(i--);
			}
			else {
				previous = breakPoints.get(i);
			}
		}
	}
	
	protected Point getLastBreakPoint() {
		Point last = getStart();
		for (Point breakPoint : breakPoints) {
			last = breakPoint;
	}
		return last;
	}
	
	protected void drawSegmentedLine(Graphics g) {
		g.setColor(color);
		Point previous = getStart();
		for (Point breakPoint : breakPoints) {
			g.drawLine(previous.x, previous.y, breakPoint.x, breakPoint.y);
			previous = breakPoint;
		}
		g.drawLine(previous.x, previous.y, getEnd().x, getEnd().y);
	}
}
